﻿using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Collections.Specialized;
using System.Windows.Controls;
using System.Windows.Media;

namespace Notoric.ViewModel.Internal
{
    /// <summary>
    /// Helper class for <see cref="IProjectExplorerItem"/> implementation.
    /// </summary>
    internal class ProjectExplorerItemHelper : IProjectExplorerItem
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ProjectExplorerItemHelper"/> instance.
        /// </summary>
        /// <param name="parent">
        /// <para>
        /// Type: <see cref="IProjectExplorerItem"/>
        /// </para>
        /// <para>
        /// The parent of this item.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="IProjectExplorerItem"/>
        /// </para>
        /// <para>
        /// The owner of this item.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        public ProjectExplorerItemHelper(IProjectExplorerItem parent, IProjectExplorerItem owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            this.owner = owner;
            children = new ObservableCollection<IProjectExplorerItem>();
            children.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(children_CollectionChanged);
        }

        #endregion

        #region IProjectExplorerItem Members

        /// <inheritdoc/>
        public ContextMenu ContextMenu
        {
            get { return contextMenu; }
        }

        /// <inheritdoc/>
        public string DisplayName
        {
            get { return null; }
        }

        /// <inheritdoc/>
        public ImageSource Icon
        {
            get { return icon; }
        }

        /// <inheritdoc/>
        public bool IsExpanded
        {
            get
            {
                return isExpanded;
            }
            set
            {
                isExpanded = value;
                if (isExpanded && parent != null && !parent.IsExpanded)
                    parent.IsExpanded = true;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("IsExpanded"));
            }
        }

        /// <inheritdoc/>
        public bool IsInEditMode
        {
            get { return isInEditMode; }
            set 
            {
                isInEditMode = value;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("IsInEditMode"));
            }
        }

        /// <inheritdoc/>
        public bool IsSelected
        {
            get
            {
                return isSelected;
            }
            set
            {
                isSelected = value;
                if (isSelected && parent != null && !parent.IsExpanded)
                    parent.IsExpanded = true;

                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
            }
        }

        /// <inheritdoc/>
        public string Name
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        /// <inheritdoc/>
        public IProjectExplorerItem Parent
        {
            get { return parent; }
            set 
            {
                parent = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Parent"));
            }
        }

        /// <inheritdoc/>
        public ObservableCollection<IProjectExplorerItem> Children
        {
            get { return children; }
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when the value of some property has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the <see cref="children"/> collection has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler modifies the <see cref="IProjectExplorerItem.Parent"/> property of the added / removed items.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void children_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (IProjectExplorerItem item in e.NewItems)
                    item.Parent = owner;
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (IProjectExplorerItem item in e.OldItems)
                    item.Parent = null;
            }
            else if (e.Action == NotifyCollectionChangedAction.Replace)
            {
                foreach (IProjectExplorerItem item in e.OldItems)
                    item.Parent = null;
                foreach (IProjectExplorerItem item in e.NewItems)
                    item.Parent = owner;
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Descends the directory hierarchy following the path segments and creates new directories when necessary.
        /// </summary>
        /// <param name="actual">Actual directory</param>
        /// <param name="segments">Path segments</param>
        /// <param name="index">Zero-based index of the actual path segment</param>
        /// <param name="directoryMenu">The context menu for the created directories</param>
        /// <returns>The leaf directory (corresponds to the last path segment)</returns>
        private Directory Descend(Directory actual, string[] segments, int index, ContextMenu directoryMenu)
        {
            var directory = (Directory)actual.Children.SingleOrDefault(dir => dir.Name == segments[index]);
            if (directory == null)
            {
                directory = new Directory(actual, segments[index], directoryMenu);
                actual.Children.Add(directory);
            }

            if (index < segments.Length - 1)
                return Descend(directory, segments, index + 1, directoryMenu);
            else
                return directory;
        }

        /// <summary>
        /// Gets the name part (last segment) of the path.
        /// </summary>
        /// <param name="path">The path</param>
        /// <returns>The name</returns>
        private string GetName(string path)
        {
            return path.Substring(path.LastIndexOf('/') + 1);
        }

        /// <summary>
        /// Gets the path part (everything except the last segment) of the path.
        /// </summary>
        /// <param name="path">The path</param>
        /// <returns>The path part of an empty string ("") if this part is empty</returns>
        private string GetPath(string path)
        {
            int lastSlash = path.LastIndexOf('/');
            
            if (lastSlash == -1)
                return "";

            if (path[0] == '/')
            {
                if (lastSlash > 0)
                    return path.Substring(1, lastSlash - 1);

                return "";
            }

            return path.Substring(0, lastSlash);
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Adds a given item to the hierarchy at the position given by a virtual path.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="String"/>
        /// </para>
        /// <para>
        /// The virtual path to the inserted item. Its root is supposed to be at <paramref name="root"/>.
        /// </para>
        /// </param>
        /// <param name="item">
        /// <para>
        /// Type: <see cref="IProjectExplorerItem"/>
        /// </para>
        /// <para>
        /// The inserted item.
        /// </para>
        /// </param>
        /// <param name="root">
        /// <para>
        /// Type: <see cref="Directory"/>
        /// </para>
        /// <para>
        /// The root of the hierarchy.
        /// </para>
        /// </param>
        /// <param name="directoryMenu">
        /// <para>
        /// Type: <see cref="ContextMenu"/>
        /// </para>
        /// <para>
        /// The context menu that will be added to newly created directories on the path.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="virtualPath"/>, <paramref name="item"/> or <paramref name="root"/> is <see langword="null"/>.
        /// </exception>
        public void AddToHierarchy(string virtualPath, IProjectExplorerItem item, Directory root, ContextMenu directoryMenu)
        {
            if (virtualPath == null)
                throw new ArgumentNullException("virtualPath");
            if (item == null)
                throw new ArgumentNullException("item");
            if (root == null)
                throw new ArgumentNullException("root");

            var path = GetPath(virtualPath);
            var dir = root;
            if (path != "")
            {
                var pathSegments = path.Split('/');
                dir = Descend(root, pathSegments, 0, directoryMenu);
            }
            dir.Children.Add(item);
        }

        /// <summary>
        /// Creates the hierarchy corresponding to the virtual paths in a given path collection and adds created items to the <see cref="Children"/> collection.
        /// </summary>
        /// <param name="pathCollection">
        /// <para>
        /// Type: <see cref="Model.Collections.PathCollection"/>
        /// </para>
        /// <para>
        /// The path collection containing the virtual paths.
        /// </para>
        /// </param>
        /// <param name="leafElementCreator">
        /// <para>
        /// Type: <see cref="LeafElementCreator"/>
        /// </para>
        /// <para>
        /// The method that will create the leaf elements (adapters for the model parts).
        /// </para>
        /// </param>
        /// <param name="directoryMenu">
        /// <para>
        /// Type: <see cref="ContextMenu"/>
        /// </para>
        /// <para>
        /// The context menu for directories in the created hierarchy.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="pathCollection"/> or <paramref name="leafElementCreator"/> is <see langword="null"/>.
        /// </exception>
        public void CreateHierarchy(Model.Collections.PathCollection pathCollection, LeafElementCreator leafElementCreator, ContextMenu directoryMenu)
        {
            if (pathCollection == null)
                throw new ArgumentNullException("pathCollection");
            if (leafElementCreator == null)
                throw new ArgumentNullException("leafElementCreator");

            foreach (Notoric.Model.Collections.PathElement pathElement in pathCollection)
            {
                string path = GetPath(pathElement.VirtualPath);
                string name = GetName(pathElement.VirtualPath);
                Directory dir;

                if (path != "")
                {
                    var pathSegments = path.Split('/');

                    var root = (Directory)children.SingleOrDefault(d => d.Name == pathSegments[0]);
                    if (root == null)
                    {
                        root = new Directory(owner, pathSegments[0], directoryMenu);
                        children.Add(root);
                    }
                    dir = Descend(root, pathSegments, 0, directoryMenu);
                    dir.Children.Add(leafElementCreator(name, pathElement.URI));
                }
                else
                    children.Add(leafElementCreator(name, pathElement.URI));
            }
        }

        /// <summary>
        /// Creates the hierarchy corresponding to the virtual paths in a given path collection with a root in a new directory with a given name.
        /// The method also adds the root directory to the <see cref="Children"/> collection.
        /// </summary>
        /// <param name="pathCollection">
        /// <para>
        /// Type: <see cref="Model.Collections.PathCollection"/>
        /// </para>
        /// <para>
        /// The path collection containing the virtual paths.
        /// </para>
        /// </param>
        /// <param name="rootDirectoryName">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// The name of the root directory.
        /// </para>
        /// </param>
        /// <param name="leafElementCreator">
        /// <para>
        /// Type: <see cref="LeafElementCreator"/>
        /// </para>
        /// <para>
        /// The method that will create the leaf elements (adapters for the model parts).
        /// </para>
        /// </param>
        /// <param name="directoryMenu">
        /// <para>
        /// Type: <see cref="ContextMenu"/>
        /// </para>
        /// <para>
        /// The context menu for the directories in the created hierarchy.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Directory"/>
        /// </para>
        /// <para>
        /// The root directory of the created hierarchy.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="pathCollection"/> or <paramref name="rootDirectoryName"/> or <paramref name="leafElementCreator"/> is <see langword="null"/>.
        /// </exception>
        public Directory CreateRootedHierarchy(Model.Collections.PathCollection pathCollection, string rootDirectoryName, LeafElementCreator leafElementCreator, ContextMenu directoryMenu)
        {
            if (pathCollection == null)
                throw new ArgumentNullException("pathCollection");
            if (rootDirectoryName == null)
                throw new ArgumentNullException("rootDirectoryName");
            if (leafElementCreator == null)
                throw new ArgumentNullException("leafElementCreator");

            var rootDirectory = new Directory(owner, rootDirectoryName, directoryMenu);

            foreach (Notoric.Model.Collections.PathElement pathElement in pathCollection)
            {
                string path = GetPath(pathElement.VirtualPath);
                string name = GetName(pathElement.VirtualPath);
                Directory dir;

                if (path != "")
                {
                    var pathSegments = path.Split('/');

                    dir = Descend(rootDirectory, pathSegments, 0, directoryMenu);
                }
                else
                    dir = rootDirectory;
    
                dir.Children.Add(leafElementCreator(name, pathElement.URI));
            }

            children.Add(rootDirectory);
            return rootDirectory;
        }

        /// <summary>
        /// Finds the project explorer item that corresponds to a given path.
        /// </summary>
        /// <param name="virtualPath">
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// The path.
        /// </para>
        /// </param>
        /// <param name="root">
        /// <para>
        /// Type: <see cref="IProjectExplorerItem"/>
        /// </para>
        /// <para>
        /// The item at which the search starts.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="IProjectExplorerItem"/>
        /// </para>
        /// <para>
        /// The item that corresponds to the <paramref name="virtualPath"/>
        /// or <see langword="null"/> if the item was not found.
        /// </para>
        /// </returns>
        public static IProjectExplorerItem FindItem(string virtualPath, IProjectExplorerItem root)
        {
            if (virtualPath == null || virtualPath == "")
                throw new ArgumentNullException("virtualPath");
            if (root == null)
                throw new ArgumentNullException("rootDirectory");

            var actualItem = root;

            var path = virtualPath.Substring(0, virtualPath.LastIndexOf('/'));
            var name = virtualPath.Substring(virtualPath.LastIndexOf('/') + 1);
            if (path != "")
            {
                if (path[0] == '/')
                    path = path.Substring(1);
                var pathSegments = path.Split('/');
                foreach (var segment in pathSegments)
                {
                    var nextItem = actualItem.Children.FirstOrDefault(child => child.Name == segment);
                    if (nextItem == null)
                        return null;

                    actualItem = nextItem;
                }
            }

            return actualItem.Children.FirstOrDefault(child => child.Name == name);
        }

        /// <summary>
        /// Gets the project that owns a given project explorer item.
        /// </summary>
        /// <param name="item">
        /// <para>
        /// Type: <see cref="Elements.Project"/>
        /// </para>
        /// <para>
        /// The project explorer item from which the search starts.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Elements.Project"/>
        /// </para>
        /// <para>
        /// The <see cref="Elements.Project"/> that owns the <paramref name="item"/>
        /// or <see langword="null"/> if this item is not owned by a project.
        /// </para>
        /// </returns>
        public static Elements.Project GetProject(IProjectExplorerItem item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            while (item != null && !(item is Elements.Project))
                item = item.Parent;

            return item as Elements.Project;
        }

        /// <summary>
        /// Sets the context menu for this project explorer item.
        /// </summary>
        /// <param name="menu">
        /// <para>
        /// Type: <see cref="ContextMenu"/>
        /// </para>
        /// <para>
        /// The context menu for this project explorer item.
        /// </para>
        /// </param>
        public void SetContextMenu(ContextMenu menu)
        {
            contextMenu = menu;

            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("ContextMenu"));
        }

        /// <summary>
        /// Sets the icon for this project explorer item.
        /// </summary>
        /// <param name="icon">
        /// <para>
        /// Type: <see cref="ImageSource"/>
        /// </para>
        /// <para>
        /// The icon for this project explorer item.
        /// </para>
        /// </param>
        public void SetIcon(ImageSource icon)
        {
            this.icon = icon;

            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("Icon"));
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="ContextMenu"/> property.
        /// </summary>
        private ContextMenu contextMenu;
        /// <summary>
        /// Holds the value of the <see cref="Icon"/> property.
        /// </summary>
        private ImageSource icon;
        /// <summary>
        /// Holds the value of the <see cref="IsExpanded"/> property.
        /// </summary>
        private bool isExpanded;
        /// <summary>
        /// Holds the value of the <see cref="IsInEditMode"/> property.
        /// </summary>
        private bool isInEditMode;
        /// <summary>
        /// Holds the value of the <see cref="IsSelected"/> property.
        /// </summary>
        private bool isSelected;
        /// <summary>
        /// Holds the owner of this item.
        /// </summary>
        private IProjectExplorerItem owner;
        /// <summary>
        /// Holds the value of the <see cref="Parent"/> property.
        /// </summary>
        private IProjectExplorerItem parent;
        /// <summary>
        /// Holds the value of the <see cref="Children"/> property.
        /// </summary>
        private ObservableCollection<IProjectExplorerItem> children;

        #endregion
    }

    /// <summary>
    /// Represents a method that will create the leaf element of a hierarchy built by <see cref="ProjectExplorerItemHelper"/>.
    /// </summary>
    /// <param name="name">
    /// <para>
    /// Type: <see cref="System.String"/>
    /// </para>
    /// <para>
    /// The name of the leaf element
    /// </para>
    /// </param>
    /// <param name="partUri">
    /// <para>
    /// Type: <see cref="Uri"/>
    /// </para>
    /// <para>
    /// The URI of the package part that will be adapted by the leaf element.
    /// </para>
    /// </param>
    /// <returns>
    /// <para>
    /// Type: <see cref="IProjectExplorerItem"/>
    /// </para>
    /// <para>
    /// The leaf element.
    /// </para>
    /// </returns>
    internal delegate IProjectExplorerItem LeafElementCreator(string name, Uri partUri);
}
